/* NOTICE
    OpenJaWS - Open Java Weather Station
    
    Copyright (C) 2008 Grant Gardner <grant@lastweekend.com.au>
    
    OpenJaWS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    
NOTICE */

package au.com.lastweekend.ws2300;

import java.io.IOException;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import au.com.lastweekend.openjaws.api.CurrentMinMax;
import au.com.lastweekend.openjaws.api.Readings;
import au.com.lastweekend.openjaws.api.UnitValue;
import au.com.lastweekend.openjaws.api.WindCurrentMinMax;
import au.com.lastweekend.openjaws.units.PercentUnit;
import au.com.lastweekend.openjaws.units.PressureUnit;
import au.com.lastweekend.openjaws.units.RainfallUnit;
import au.com.lastweekend.openjaws.units.TemperatureUnit;
import au.com.lastweekend.openjaws.units.WindUnit;
import au.com.lastweekend.ws2300.ForecastInfo.Forecast;
import au.com.lastweekend.ws2300.ForecastInfo.Tendency;

/**
 * Turns the raw device memory into useful functions to read and write station data.
 * <p>
 * 
 * See <a href="http://open2300.org/memory-map">this memory map</a> for the layout of data in the WS2300 memory.
 * 
 * @author Grant Gardner <grant@lastweekend.com.au>
 */
public class WS2300 {

    /**
     * Encapsulates history records stored in the WS2300 memory.
     * 
     * On construction will check if the station is within 1 minute of writing a record In that case it will start at the second
     * available record.
     * 
     * ie to ensure accurate data you need to read the 0 record within 1 minute of obtaining a HistoryInfo the remaining records
     * need to be read within recordNum * historyInterval minutes.
     * 
     * Typical usage would be
     * 
     * <code>
     * HistoryInfo history = ws2300.getHistoryInfo();
     * Readings readings = new Readings();
     * int numRecords = history.getNumRecords();
     * 
     * for (int i = 0; i <= numRecords ; i++) {
     *      history.getRecord(i,readings);
     *      //Do something with the readings...
     * }
     * </code>
     * 
     * @author ggardner
     * 
     */
    public class HistoryInfo {

        private static final int MAX_HISTORY_RECORDS = 175;

        private int historyInterval;
        private Date lastRecordTime;
        private int numRecords;
        private int firstRecord;
        private int minutesToNextReading;

        public HistoryInfo() throws IOException {

            byte[] nibbles = device.read(MemoryMap.HISTORY_INFO, 10);

            historyInterval = (int) Device.binary(nibbles, 0, 3) + 1;
            minutesToNextReading = (int) Device.binary(nibbles, 3, 3) + 1;
            lastRecordTime = datetime(nibbles, 6);
            int lastRecord = (int) Device.binary(nibbles, 16, 2);
            numRecords = (int) Device.binary(nibbles, 18, 2);

            if (minutesToNextReading <= 1 && numRecords == MAX_HISTORY_RECORDS) {
                // We're about to overwrite the first record. Start at the
                // second.
                numRecords--;
            }

            firstRecord = (MAX_HISTORY_RECORDS + lastRecord - numRecords + 1) % MAX_HISTORY_RECORDS;

        }

        public int getNumRecords() {

            return numRecords;
        }

        public void getRecord(int currentRecord, Readings readings) throws IOException {

            if (currentRecord < 0 || currentRecord >= numRecords) {
                throw new IndexOutOfBoundsException("Index " + currentRecord + " out of range 0 - " + numRecords);
            }
            int recordPointer = ((firstRecord + currentRecord) % MAX_HISTORY_RECORDS);

            long readingTimeMillis = lastRecordTime.getTime() - (historyInterval * (numRecords - currentRecord - 1) * 60 * 1000);

            Date readingTime = new Date(readingTimeMillis);

            readings.setReadingTime(readingTime);

            byte[] nibbles;

            int recordAddress = MemoryMap.HISTORY_RECORDS + (recordPointer * 19);

            nibbles = device.read(recordAddress, 10);

            long compressedValue = Device.binary(nibbles, 0, 5);
            double indoorTemp = (((double) (compressedValue % 1000) / 10.0)) - 30.0;
            double outDoorTemp = (((double) (compressedValue - (compressedValue % 1000))) / 10000.0) - 30.0;

            compressedValue = Device.binary(nibbles, 5, 5);
            double airPressure = 1000.0 + (((double) (compressedValue % 10000)) / 10.0);
            if (airPressure > 1500.0) {
                airPressure = airPressure - 1000.0;
            }
            airPressure += getAirPressureCorrection().convertTo(PressureUnit.HECTOPASCAL).getValue();
            double indoorHumidity = ((double) (compressedValue - (compressedValue % 10000))) / 10000.0;
            double outdoorHumidity = Device.bcd(nibbles, 10, 2);

            double windSpeed = ((double) Device.binary(nibbles, 15, 3)) / 10.0;
            double windDirection = nibbles[18] * 22.5;

            double rainAbsolute = getRainPerImpulse().convertTo(RainfallUnit.MILLIMETRE).getValue()
                    * Device.binary(nibbles, 12, 3);

            readings.getIndoorTemperature().setCurrent(indoorTemp, TemperatureUnit.CELSIUS, readingTime);
            readings.getOutdoorTemperature().setCurrent(outDoorTemp, TemperatureUnit.CELSIUS, readingTime);
            readings.getAirPressure().setCurrent(airPressure, PressureUnit.HECTOPASCAL, readingTime);
            readings.getIndoorHumidity().setCurrent(indoorHumidity, PercentUnit.PERCENT, readingTime);
            readings.getOutdoorHumidity().setCurrent(outdoorHumidity, PercentUnit.PERCENT, readingTime);
            readings.getWindSpeed().setCurrent(windSpeed, WindUnit.METRES_PER_SECOND, windDirection, readingTime);
            readings.setRainAbsolute(rainAbsolute, RainfallUnit.MILLIMETRE);

        }

        /**
         * Will possibly return an index >= getNumRecords, indicating no history after the supplied date.
         * 
         * @param fromDate
         * @return the index for the first history record after the supplied fromDate.
         */
        public int getIndex(Date fromDate) {

            if (fromDate == null) {
                return 0;
            }

            if (lastRecordTime.before(fromDate)) {
                return numRecords;
            }

            long diffMillis = lastRecordTime.getTime() - fromDate.getTime();
            long histMillis = historyInterval * 60 * 1000;

            int index = (int) (numRecords - (diffMillis / histMillis) - Long.signum(diffMillis % histMillis));

            return (index < 0) ? 0 : index;
        }

        /**
         * 
         * @return the interval (minutes) between history saves
         */
        public int getInterval() {

            return historyInterval;
        }

        public int getMinutesToNextReading() {

            return minutesToNextReading;
        }
    }

    private static final Logger LOG = LoggerFactory.getLogger(WS2300.class);

    private Device device;

    // TODO Find a way to detect use of DCF77, in which case the synchronisation
    // should be disabled.
    private boolean usingDCF77 = false;

    private UnitValue pressureCorrection = null;
    private UnitValue rainPerImpulse = null;

    public WS2300(Device device) {

        this.device = device;
    }

    public HistoryInfo getHistoryInfo() throws IOException {

        return new HistoryInfo();

    }

    /**
     * Reset the history interval.
     * <p>
     * Because the times are relative to the last record, changing the historyInterval invalidates all the previous data. We reset
     * the number of records back to zero and set the station to start recording again.
     * <p>
     * If you are trying to align the minutesToNextReading with a particular time, you should check that your calculations have all
     * executed in the same minute.
     * 
     * @param historyInterval
     * @param minutesToNextReading
     * @throws IOException
     */
    public void resetHistoryInfo(int historyInterval, int minutesToNextReading) throws IOException {

        resetHistoryInfo(historyInterval, minutesToNextReading, true);
    }

    protected void resetHistoryInfo(int historyInterval, int minutesToNextReading, boolean zeroNumRecords) throws IOException {

        historyInterval--;
        minutesToNextReading--;

        byte[] nibbles = new byte[6];
        Device.binaryEncode(historyInterval, nibbles, 0, 3);
        Device.binaryEncode(minutesToNextReading, nibbles, 3, 3);
        device.write(MemoryMap.HISTORY_INFO, nibbles);

        if (zeroNumRecords) {
            // Zero the numRecords
            nibbles = new byte[2];
            device.write(MemoryMap.HISTORY_INFO + 18, nibbles);
        }
    }

    /**
     * 
     * @return number of seconds before device next takes a reading from the outdoor sensors
     * @throws IOException
     */
    public int getSecondsToNextAvailableReading() throws IOException {

        byte[] nibbles = device.read(MemoryMap.NEXT_READING, 1);

        return (int) ((Device.binary(nibbles, 0, 2)) / 2) + 1;
    }

    /**
     * @see #getCalendarTime()
     * @return local time
     * @throws IOException
     */
    public Date getCurrentTime() throws IOException {

        return getCalendarTime().getTime();

    }

    /**
     * @return station's understanding of local time.
     * 
     * TODO: Use Timezone to do a UTC offset for DCF users.
     * 
     * @throws IOException
     */
    public Calendar getCalendarTime() throws IOException {

        byte[] nibbles = device.read(MemoryMap.TIME_UTC, 7);
        return calendar(nibbles, 0, true, true);

    }

    /**
     * 
     * Current/Max/Min in degrees Celsius with precision of 0.1.
     * 
     * @param reading
     * @param readingTime
     * @throws IOException
     */
    public void getIndoorTemperature(CurrentMinMax reading, Date readingTime) throws IOException {

        getTemperature(MemoryMap.INDOOR_TEMPERATURE, reading, readingTime);
    }

    /**
     * Current/Max/Min in degrees Celsius with precision of 0.1.
     * 
     * @param reading
     * @param readingTime
     * 
     * @throws IOException
     */
    public void getOutdoorTemperature(CurrentMinMax reading, Date readingTime) throws IOException {

        getTemperature(MemoryMap.OUTDOOR_TEMPERATURE, reading, readingTime);
    }

    /**
     * Station's calculation of dewpoint.
     * 
     * Current/Max/Min in degrees Celsius with precision of 0.1.
     * 
     * @param reading
     * @param readingTime
     * 
     * @throws IOException
     */
    public void getDewPoint(CurrentMinMax reading, Date readingTime) throws IOException {

        getTemperature(MemoryMap.DEWPOINT, reading, readingTime);
    }

    /**
     * Station's calculation of windchill.
     * 
     * Current/Max/Min in degrees Celsius with precision of 0.1.
     * 
     * @param reading
     * @param readingTime
     * 
     * @throws IOException
     */
    public void getWindChill(CurrentMinMax reading, Date readingTime) throws IOException {

        getTemperature(MemoryMap.WINDCHILL, reading, readingTime);
    }

    /**
     * 
     * Current/Max/Min percentage with precision of 1.0%
     * 
     * @param reading
     * @param readingTime
     * 
     * @throws IOException
     */
    public void getIndoorHumidity(CurrentMinMax reading, Date readingTime) throws IOException {

        getHumidity(MemoryMap.INDOOR_HUMIDITY, reading, readingTime);
    }

    /**
     * 
     * Current/Max/Min percentage with precision of 1.0%
     * 
     * @param reading
     * @param readingTime
     * 
     * @throws IOException
     */
    public void getOutdoorHumidity(CurrentMinMax reading, Date readingTime) throws IOException {

        getHumidity(MemoryMap.OUTDOOR_HUMIDITY, reading, readingTime);
    }

    /**
     * Relative pressure = Absolute - Pressure Correction.
     * 
     * Current/Max/Min in hPa with precision of 0.1
     * 
     * @param reading
     * @param readingTime
     * 
     * @throws IOException
     */
    public void getRelativeAirPressure(CurrentMinMax reading, Date readingTime) throws IOException {

        getAirPressure(true, reading, readingTime);

    }

    /**
     * Absolute pressure as measured by the station.
     * 
     * Current/Max/Min in hPa with precision of 0.1
     * 
     * @param reading
     * @param readingTime
     * 
     * @throws IOException
     */
    public void getAbsoluteAirPressure(CurrentMinMax reading, Date readingTime) throws IOException {

        getAirPressure(false, reading, readingTime);
    }

    /**
     * Pressure correction stored in the station
     * 
     * @return hPa with precision of 0.1
     * @throws IOException
     */
    public UnitValue getAirPressureCorrection() throws IOException {

        if (pressureCorrection == null) {
            double pressureCorrectionHPa = pressure(device.read(MemoryMap.AIR_PRESSURE_CORRECTION, 3)) - 1000.0;
            pressureCorrection = new UnitValue(pressureCorrectionHPa, PressureUnit.HECTOPASCAL);
        }
        return pressureCorrection;

    }

    /**
     * 
     * @param pressureCorrection
     * @param unit
     *            of pressure that the correction value is in
     * @throws IOException
     */
    public void setAirPressureCorrection(double pressureCorrection, PressureUnit unit) throws IOException {

        double pressureOffset = PressureUnit.HECTOPASCAL.convertFrom(pressureCorrection, unit);

        pressureOffset += 1000.0;
        pressureOffset = pressureOffset * 10.0;

        byte[] nibbles = new byte[5];
        Device.bcdEncode((int) pressureOffset, nibbles, 0, 5);
        device.write(MemoryMap.AIR_PRESSURE_CORRECTION, nibbles);

        // Read from the station next time we need this.
        this.pressureCorrection = null;

    }

    /**
     * This value is generally 0.518mm.
     * 
     * @return The amount of rain represented by one "tip" of the rain gauge bucket
     * @throws IOException
     */
    public UnitValue getRainPerImpulse() throws IOException {

        if (rainPerImpulse == null) {
            byte[] nibbles = device.read(MemoryMap.RAIN_PER_IMPULSE, 2);
            double mmPerImpulse = Device.binary(nibbles, 0, 3) / 1000.0;
            rainPerImpulse = new UnitValue(mmPerImpulse, RainfallUnit.MILLIMETRE);
        }

        return rainPerImpulse;

    }

    /**
     * Station keeps track of rain in a counter of "impulses". This is the absolute value since the outdoor sensor was last
     * restarted.
     * <p>
     * It would be handy if the station stored the date/time when this occured but we haven't found that yet.
     * <p>
     * TODO: Find if the station stores such a reset date.
     * 
     * @return station rainfall, mm in increments of {@link #getRainPerImpulse()}
     * @throws IOException
     */
    public UnitValue getRainAbsolute() throws IOException {

        long impulseCount = Device.binary(device.read(MemoryMap.RAIN_COUNTER, 2), 0, 3);
        double mmRain = impulseCount * getRainPerImpulse().convertTo(RainfallUnit.MILLIMETRE).getValue();
        return new UnitValue(mmRain, RainfallUnit.MILLIMETRE);
    }

    /**
     * Total Rainfall Current rainfall in mm
     * 
     * @param reading
     * @param readingTime
     * 
     * @throws IOException
     */
    public void getRainTotalSinceReset(CurrentMinMax reading, Date readingTime) throws IOException {

        byte[] nibbles = device.read(MemoryMap.RAIN_TOTAL, 8);

        reading.setCurrent(rain(nibbles, 0), RainfallUnit.MILLIMETRE, readingTime);
        if (!isCurrentOnly(reading)) {
            reading.setMinimum(0, RainfallUnit.MILLIMETRE, datetime(nibbles, 6));
        }

    }

    /**
     * Rolling hourly rainfall in mm. Current and Max
     * 
     * @param reading
     * @param readingTime
     * 
     * 
     * @throws IOException
     */
    public void getRain1Hour(CurrentMinMax reading, Date readingTime) throws IOException {

        getRain(MemoryMap.RAIN_1_HR, reading, readingTime);
    }

    /**
     * Rolling 24Hr rainfall in mm. Current and Max
     * 
     * @param reading
     * @param readingTime
     * 
     * @throws IOException
     */
    public void getRain24Hours(CurrentMinMax reading, Date readingTime) throws IOException {

        getRain(MemoryMap.RAIN_24_HRS, reading, readingTime);
    }

    /**
     * Wind speed in m/s
     * 
     * @param reading
     * @param readingTime
     * 
     * @throws IOException
     */
    public void getWindSpeed(WindCurrentMinMax reading, Date readingTime) throws IOException {

        byte[] nibbles = device.read(MemoryMap.WIND_CURRENT, 3);

        // rawWindSpeed in 10s of m/s
        long rawWindSpeed = Device.binary(nibbles, 2, 3);

        // ignore overflow conditions and the known bad electrical interference
        // value
        if (nibbles[0] != 0 || nibbles[1] != 0 || rawWindSpeed == 0x01FF || rawWindSpeed == 0x00FF || rawWindSpeed == 0x01FE) {
            LOG.warn("Interference or overflow reading wind-speed");
        } else {
            double direction = ((double) nibbles[5]) * 22.5;
            reading.setCurrent(rawWindSpeed / 10.0, WindUnit.METRES_PER_SECOND, direction, readingTime);
        }

        if (!isCurrentOnly(reading)) {
            nibbles = device.read(MemoryMap.WIND_MINMAX, 15);

            // Station doesn't keep direction for max/min
            reading.setMaximum(wind(nibbles, 6), WindUnit.METRES_PER_SECOND, datetime(nibbles, 20));
            reading.setMinimum(wind(nibbles, 0), WindUnit.METRES_PER_SECOND, datetime(nibbles, 10));
        }

    }

    public double[] getWindDirections() throws IOException {

        byte[] nibbles = device.read(MemoryMap.WIND_DIRECTION, 3);
        double[] directions = new double[6];
        for (int i = 0; i < 6; i++) {
            directions[i] = ((double) nibbles[i]) * 22.5;
        }
        return directions;
    }

    public ForecastInfo getForecast() throws IOException {

        byte[] nibbles = device.read(MemoryMap.FORECAST, 1);
        Forecast forecast;
        switch (nibbles[0]) {
            case 0 :
                forecast = Forecast.RAINY;
                break;
            case 1 :
                forecast = Forecast.CLOUDY;
                break;
            case 2 :
                forecast = Forecast.SUNNY;
                break;
            default :
                forecast = Forecast.UNKNOWN;
        }

        Tendency tendency;
        switch (nibbles[1]) {
            case 0 :
                tendency = Tendency.STEADY;
                break;
            case 1 :
                tendency = Tendency.RISING;
                break;
            case 2 :
                tendency = Tendency.FALLING;
                break;
            default :
                tendency = Tendency.UNKNOWN;
        }

        return new ForecastInfo(forecast, tendency);
    }

    /**
     * Sets the time. TODO: Handle timezone and drift from DCF77.
     * 
     * @param date
     * @throws IOException
     */
    public void setCurrentTime(Date date) throws IOException {

        if (usingDCF77) {
            LOG.debug("Relying on DCF77, skipping sync");
            return;
        }

        Calendar cal = Calendar.getInstance();
        cal.clear();
        cal.setTimeZone(TimeZone.getDefault());
        cal.setTime(date);

        setCurrentTime(cal);

    }

    protected void setCurrentTime(Calendar cal) throws IOException {

        // Note. We do not set the time at MemoryMap.DATE_LAST_SET, this is used
        // by the station when setting manually. Would be interested to know
        // what happens with DATE_LAST_SET for DCF77 users.

        int address = MemoryMap.TIME_UTC;
        setTime(address, cal);
    }

    protected void setTime(int address, Calendar cal) throws IOException {

        byte[] nibbles = new byte[13];
        Device.bcdEncode(cal.get(Calendar.SECOND), nibbles, 0, 2);
        Device.bcdEncode(cal.get(Calendar.MINUTE), nibbles, 2, 2);
        Device.bcdEncode(cal.get(Calendar.HOUR_OF_DAY), nibbles, 4, 2);
        Device.bcdEncode((((cal.get(Calendar.DAY_OF_WEEK) + 6) % 7) + 1), nibbles, 6, 1);
        Device.bcdEncode(cal.get(Calendar.DAY_OF_MONTH), nibbles, 7, 2);
        Device.bcdEncode(cal.get(Calendar.MONTH) + 1, nibbles, 9, 2);
        Device.bcdEncode(cal.get(Calendar.YEAR) - 2000, nibbles, 11, 2);

        device.write(address, nibbles);
    }

    private void getRain(int address, CurrentMinMax reading, Date readingTime) throws IOException {

        byte[] nibbles;
        if (isCurrentOnly(reading)) {
            nibbles = device.read(address, 3);
        } else {
            nibbles = device.read(address, 11);
        }
        reading.setCurrent(rain(nibbles, 0), RainfallUnit.MILLIMETRE, readingTime);

        if (!isCurrentOnly(reading)) {
            reading.setMaximum(rain(nibbles, 6), RainfallUnit.MILLIMETRE, datetime(nibbles, 12));
        }
    }

    private void getAirPressure(boolean relative, CurrentMinMax reading, Date readingTime) throws IOException {

        int offset = 0;
        if (relative) {
            offset = 10;
        }

        double current = pressure(device.read(MemoryMap.AIR_PRESSURE + offset, 3));

        reading.setCurrent(current, PressureUnit.HECTOPASCAL, readingTime);

        if (!isCurrentOnly(reading)) {
            double min = pressure(device.read(MemoryMap.AIR_PRESSURE_MIN + offset, 3));
            double max = pressure(device.read(MemoryMap.AIR_PRESSURE_MIN + offset + 20, 3));

            // times are of course in the same place for relative/absolute
            byte[] nibbles = device.read(MemoryMap.AIR_PRESSURE_MIN_TIME, 10);

            Date timeMin = datetime(nibbles, 0);
            Date timeMax = datetime(nibbles, 10);
            reading.setMinimum(min, PressureUnit.HECTOPASCAL, timeMin);
            reading.setMaximum(max, PressureUnit.HECTOPASCAL, timeMax);
        }

    }

    private void getHumidity(int address, CurrentMinMax reading, Date readingTime) throws IOException {

        byte[] nibbles;
        if (isCurrentOnly(reading)) {
            nibbles = device.read(address, 1);
        } else {
            nibbles = device.read(address, 13);
        }

        reading.setCurrent(humidity(nibbles, 0), PercentUnit.PERCENT, readingTime);
        if (!isCurrentOnly(reading)) {
            reading.setMinimum(humidity(nibbles, 2), PercentUnit.PERCENT, datetime(nibbles, 6));
            reading.setMaximum(humidity(nibbles, 4), PercentUnit.PERCENT, datetime(nibbles, 16));
        }
    }

    private CurrentMinMax getTemperature(int address, CurrentMinMax reading, Date readingTime) throws IOException {

        byte[] temps;
        byte[] dates = null;
        if (isCurrentOnly(reading)) {
            temps = device.read(address, 2);
        } else {
            temps = device.read(address, 7);
            dates = device.read(address + 14, 10);

        }

        reading.setCurrent(temperature(temps, 0), TemperatureUnit.CELSIUS, readingTime);
        if (!isCurrentOnly(reading)) {
            reading.setMaximum(temperature(temps, 10), TemperatureUnit.CELSIUS, datetime(dates, 10));
            reading.setMinimum(temperature(temps, 5), TemperatureUnit.CELSIUS, datetime(dates, 0));
        }
        return reading;
    }

    private static Date datetime(byte[] nibbles, int offset) {

        return calendar(nibbles, offset, false, false).getTime();
    }

    private static Calendar calendar(byte[] nibbles, int offset, boolean skipDayOfWeek, boolean includesSeconds) {

        int seconds = 0;

        if (includesSeconds) {
            seconds = Device.bcd(nibbles, offset, 2);
            offset += 2;
        }

        int minute = Device.bcd(nibbles, offset, 2);
        int hour = Device.bcd(nibbles, offset + 2, 2);
        if (skipDayOfWeek) {
            offset = offset + 1;
        }
        int day = Device.bcd(nibbles, offset + 4, 2);
        int month = Device.bcd(nibbles, offset + 6, 2) - 1;
        int year = Device.bcd(nibbles, offset + 8, 2) + 2000;

        Calendar cal = Calendar.getInstance();
        cal.clear();
        cal.set(Calendar.MILLISECOND, 0);
        cal.setTimeZone(TimeZone.getDefault()); // Times are all local times.
        cal.set(year, month, day, hour, minute, seconds);

        return cal;

    }

    private static double wind(byte[] nibbles, int offset) {

        return ((double) Device.binary(nibbles, offset, 4)) / 360.0;
    }

    private static double rain(byte[] nibbles, int offset) {

        return (((double) Device.bcd(nibbles, offset, 6)) / 100.0);
    }

    private static double pressure(byte[] nibbles) {

        return (((double) Device.bcd(nibbles, 0, 5)) / 10.0);
    }

    private static double humidity(byte[] nibbles, int offset) {

        return (double) Device.bcd(nibbles, offset, 2);
    }

    private static double temperature(byte[] nibbles, int offset) {

        return (((double) Device.bcd(nibbles, offset, 4)) / 100.0) - 30.0;
    }

    public boolean isUsingDCF77() {

        return usingDCF77;
    }

    public void setUsingDCF77(boolean usingDCF77) {

        this.usingDCF77 = usingDCF77;
    }

    /**
     * If reading already has a max and min, then don't overwrite it with a value from the station.
     * 
     * @param reading
     * @return whether we need to read the max/min values
     */
    public boolean isCurrentOnly(CurrentMinMax reading) {

        return reading.getMax().getUnitValue() == null || reading.getMin().getUnitValue() == null;
    }

    public Device getDevice() {

        return device;
    }

}